
perf = typeof perf == "undefined" ? {} : perf ;
perf.expressions = {};
perf.exps = perf.expressions;
perf.operations = {};
perf.ops = perf.operations;

/**
 * @class 性能計測テスト
 */
perf.Tests = function(configuration) {
  this.tests = [];
  this.testsMap = {};
};
perf.Tests.prototype = {
  /**
   * @function テストを登録する。
   * @param {{
   *   id:string,
   *   operation:perf.ops.Operation,
   *   waitFor:perf.expressions.Expression
   * }} test テスト
   */
  regist : function( test ) {
    if (this.testsMap[test.id]) throw "test(" + test.id + ") is already registerd.";
    test.enable = true;
    this.tests.push( test );
    this.testsMap[test.id] = test;
  },
  /**
   * @function テストを実行するかどうかを設定する。
   * @param {string} id テストID
   * @param {boolean} enable 実行するかどうか
   */
  setEnable : function( id, enable ) {
    this.testsMap[id].enable = enable;
  },
  /**
   * @function テストの一覧を取得
   */
  getTests : function() {
    var list = [];
    for ( var i=0,n=this.tests.length;i<n;i++ ) {
      if (this.testsMap[this.tests[i].id].enable === false) continue;
      list.push( this.tests[i] );
    }
    return list;
  } 
};

/**
 * ロガー
 */
perf.Logger = function() {
  this.clearLogs();
};
perf.Logger.prototype = perf.utils.merge( new perf.utils.ListenerSupport(), {
    /**
     * @function イベントの実行を記録する。
     * @param {string} event イベント
     */
    executed : function( event ) {
      this._log( [event,  "-", this._id(event)].join("") );
    },
    /**
     * @function イベントの実行開始を記録する。
     * @param {string} event イベント
     * @return {Function} イベントの終了を記録する関数。
     */
    start : function( event ) {
      var self = this;
      var id = this._id(event);
      this._log( ["start-", event,  "-", id].join("") );
      return function() {
        self._log( ["end-", event,  "-", id].join("") )
      };
    },
    /**
     * @function ログを初期化する。
     */
    clearLogs : function() {
      this.eventIds = {};
      this.logs = {};
    },
    _id : function( event ) {
      return this.eventIds[event] = 
        (this.eventIds[event] ? this.eventIds[event] + 1 : 1);
    },
    _log : function( event ) {
      var self = this;
      setTimeout( function() {
        self.__log(event);
        self.fire( "logged", {event:event} );
      }, 0);
    },
    __log : function( event ) {
      this.logs[event] = {event:event, time:perf.utils.currentTime()};
    }
});

/**
 * コンテキスト
 */
perf.Context = function( test, logger ) {
  this.screen = new perf.Screen( "screen" );
  this.logger = logger;
  this.test = test;
  this.logs = {};
};

/**
 * スクリーン
 */
perf.Screen = function( id ) {
  this.screen = document.getElementById( id );
  this.clear();
}
perf.Screen.prototype = {
  /**
   * @function スクリーンの状態を初期化する。
   */
  clear : function() {
    this.screen.src = "";
  },
  /**
   * @function スクリーンにサイトをロードする。
   * @param {string} url サイトのURL
   */
  load : function( url ) {
    this.screen.src = url;
  },
  /**
   * @function スクリーンのグローバルオブジェクトを得る。
   * @return {Object} スクリーンのグローバルオブジェクト
   */
  screenGrobal : function() {
    return this.screen.contentWindow;
  }
}

/**
 * @class テストランナー
 * @param {Array.<Object>} test テスト
 * @param {number} iteration 試行番号
 * @param {number} cooldown 次のテスト実行までの待ち時間(ms)
 * @param {number} timeout テストを失敗とみなすまでの待ち時間(ms)
 * @param {boolean} stopPerTest テストごとに停止するかどうか
 */
perf.Runner = function(tests, iteration, cooldown, timeout, stopPerTest) {
  perf.utils.ListenerSupport.apply(this);
  this.iteration = iteration || 5;
  this.tests = tests;
  this.cooldown = cooldown || 200;
  this.timeout = (timeout || 10) * 1000;
  this.stopPerTest = stopPerTest;
  this.results = {};
  
  this.currentIndex = 0;
  this.currentIteration = 0;
};
perf.Runner.prototype = perf.utils.merge( new perf.utils.ListenerSupport(), {
  /**
   * @function テストを開始する。
   */
  start: function( ){
    this.fire( "runnerStarted", {tests:this.tests} );
    this.currentIndex = 0;
    this.currentIteration = -1;
    this._next();
  },
  _next : function() {
    if ( this.tests.length > this.currentIndex  ) {
      if ( ++this.currentIteration <= this.iteration ) {
        this._startTest( this.tests[this.currentIndex] );
      } else {
        this.currentIndex++;
        this.currentIteration = -1;
        this._next();
      }
    } else {
      this.fire( "runnerEnded", { 
        tests:this.tests, 
        results:this.results 
      });
    }
  },
  _startTest : function( test ) {
    var self = this;
    var r = new perf.TestRunner(test);
    r.addListener( "testStarted", this );
    r.addListener( "testEnded", this );
    r.addListener( "stepStarted", this );
    r.addListener( "stepEnded", this );
    r.addListener( "logged", this );
    r.start( this.currentIteration, this.timeout, function( result ){
      if ( !self.results[test.id] ) self.results[test.id] = [];
      self.results[test.id][self.currentIteration] = result;
      if ( self.stopPerTest ) alert("待機中");
      setTimeout( function(){
        self._next();
      }, self.cooldown );
    });
  },
  onTestStarted : perf.utils.ListenerSupport.prototype.relay,
  onTestEnded : perf.utils.ListenerSupport.prototype.relay,
  onStepStarted : perf.utils.ListenerSupport.prototype.relay,
  onStepEnded : perf.utils.ListenerSupport.prototype.relay,
  onLogged : perf.utils.ListenerSupport.prototype.relay
});

/**
 * @class 指定のテストを実行するランナー
 */
perf.TestRunner = function(test) {
  this.test = test;
  perf.utils.ListenerSupport.apply(this);
};
perf.TestRunner.prototype = perf.utils.merge( new perf.utils.ListenerSupport(), {
  
  start : function(iteration, timeout, callback) {
    this.iteration = iteration;
    this.callback = callback;
    var logger = new perf.Logger();
    this.context = new perf.Context(this.test, logger);
    perf.logger = this.context.logger;
    this.seq = this._createSequence(); 
    
    this.fire( "testStarted", {
      test: this.test,
      iteration: iteration
    });
    
    logger.addListener( "logged", this );
    this._startKiller( iteration, timeout );
    this._next();
  },
  _next : function() {
    if ( !this.seq.next( this.context ) ) {
      this._end("success");
    }
  },
  _end : function(status) {
    perf.logger = null;
    this.context.logger.removeListener( "logged", this );
    var result = { logs: this.context.logs, status: status};
    this.fire( "testEnded", {
      test: this.test,
      result: result, 
      iteration: this.iteration
    });
    this.iteration = null;
    this.callback(result);
  },
  _startKiller : function ( iteration, timeout ) {
    // 一定期間内にテストが完了していなければ強制的にテストを終了し次を実行する。
    var self = this;
    setTimeout( function() {
      if ( self.iteration === iteration ) {
        self.context.logger.__log( "end" );
        self.context.logs[self.currentStep] = self.context.logger.logs;
        self._end( "timeout" ); 
      }
    }, timeout );
  },
  _createSequence : function() {
    var seq = new perf.utils.Sequence();
    this._addStep( seq, this.test.setup, "setup" );
    this._addStep( seq, this.test.test, "test" );
    this._addStep( seq, this.test.teardown, "teardown" );
    return seq;
  },
  _addStep : function( seq, ops, stepId ) {
    var self = this;
    if ( !ops ) return; 
    seq.append( function(context) {
      self.currentStep = stepId;
      self.fire( "stepStarted", {
        test: self.test,
        step: stepId,
        iteration: self.iteration
      });
      context.logger.clearLogs();
      context.logger.__log( "start" );
      return true;
    } );
    seq.appendAll( ops );
    seq.append( function(context) {
      context.logger.__log( "end" );
      context.logs[stepId] = context.logger.logs;
      self.fire( "stepEnded", {
        test: self.test,
        step: stepId,
        iteration: self.iteration
      });
      return true;
    } );
  },
  onLogged : function( event ) {
    this.relay( event );
    this._next();
  }
});

perf.tests = new perf.Tests();
perf.regist = function( test ) { perf.tests.regist(test); } 
